########################################################################
#  Pyfm - personal finance data-base
#  Copyright (C) 2007,8 Ehud Ben-Reuven
#  udi@benreuven.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
########################################################################
""""Start of model definition:
See obj.py to understrand the language in which the objects are defined."""
from tagsyntax import *
from obj.obj import Obj
"""Classes for dtabase objects
=================================
"""
class SourceClass(Obj):
    """Info on a group of FILE (Source)
COMPLETE - Every account that appears in a complete source
must have at least one statement for each of its holdings.
unless the holding is zero throughout DTSTART...DTEND
FULL -  All transaction between DTSTART...(DTEND-FULL_GRACE)
appear in the source.
"""
    stags=[
        'FILECLASS',
        'ACCURACY',
        'COMPLETE',
        'FULL',
        'DTSTART',
        'DTEND',
        'HAS_ACRDINT',
        'HAS_TSOURCE',
        'FULL_GRACE',
        'FILEFILTER',
    ]
    mtags=['ORG','FILE']
    ktags=[['FILECLASS']]
    dtags=['FILECLASS','DTSTART','DTEND']
    
#class Source(Obj):
    #stags=['FILE','SourceClass']
    #ktags=[['FILE']]
    #dtags=['FILE']

class Asset(Obj):
    stags=['AIDX'] # OFX+ Numerical index to asset
    stags+=['CURRENCY'] # Default currency for UNITPRICE values for this Asset.
    # In a given line it's possible to specificy the UNITPRICE in a different currency
    # by adding the tag-value pair CURRENCY=<currency-used-in-this-line> to the line.
    stags+=['ORIGCURRENCY'] # Currency in which the asset is traded.
    # When talking about the value (UNITPRICE) of the asset in a different CURRENCY it is important
    # to remember that some of this value is a result of currency exchange rate (FX).
    # In some countires capital gain is computed in this currency.
    # (however in the US capital gain is computed using USD.)
    # In Checking accounts 1 UNIT of the Asset is 1 UNIT of ORIGCURRENCY
    # TODO Merged transactions and statements will appear in ORIGCURRENCY
    stags+=['DTMAT'] # Debt maturity date
    stags+=['FIXEDPRICE'] # For fixed price assets (in ORIGCURRENCY)
    
    mtags=['SECID'] # Asset's unique names. Usually a unique name has a structure <domain>=<name>. Examples for domains: CUSIP,ISIN,TASE...
    mtags+=['TICKER'] # A ticker does not uniquely identifies an Asset because
    # the ticker symbol may move from one Asset to another (when doing a split
    # or a merge). In addition the same asset may have several tickers (for
    # example when removing the F at the end to indicate Foreign stock.)
    mtags+=['SECNAME'] # Asset's non unique name. Either a descriptor of the
    # asset or a string that has the structure <tag>=<value>
    mtags+=['ASSETCLASS','AMEMO','DEBTTYPE',
        'COUPONFREQ', # When coupons mature. One of the following values:
        # MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL, or OTHER.
        ]
    mtags+=['COUNTRY'] # Country in which the investment is traded. May be important for tax
    ktags=[['AIDX'],['SECID']] # AIDX is first, so it will be used when
    # represnting the key of an Asset
    def __str__(self):
        # sometime SECNAME is empty so dont drop calling the super-class
        if self.AIDX:
            return str(self.AIDX[0])
        elif self.SECNAME:
            names=[str(s) for s in self.SECNAME]
        elif self.SECID:
            names=[str(s) for s in self.SECID]
        else:
            raise Exception('No key')
        return min(names,key=len)
    def ischecking(self):
        return (any(ac.ischecking() for ac in self['ASSETCLASS']) or
            any(n.startswith('CURRENT=') for n in self['SECID']))
    def isfixedprice(self):
        return (any(ac.isfixedprice() for ac in self['ASSETCLASS']) or
            any(n.startswith('CURRENT=') for n in self['SECID']))
    def getcusip(self):
        """For exporting purposes return the CUSIP number of the asset,
        If you don't know what it is, return a fake valid CUSIP number that
        will unqiuely identify it.
        """
        for s in self.SECID:
            if s.getcusip(): return s.getcusip()
        for s in self.SECID:
            if s.startswith('ISIN='):
                return s[-10:-1] # Skip the control digit
        if self.AIDX:
            return "%09d"%self.AIDX[0]
        return self.SECID[0][-9:]
    def getticker(self):
        """return the TICKER of the asset, or None"""
        if self.TICKER: return self.TICKER[0]
        #for s in self.SECID:
            #domain,subdomain,label=s.explode()
            #if domain=='TICKER': return label
    def getsecname(self):
        if self.SECNAME:
            return max(self.SECNAME,key=lambda s: len(s))
        else:
            return str(self) # Use the object's keys as a name
    def getEngSecname(self):
        secnames=[''.join(c for c in s if c.isalnum()) for s in self.SECNAME if any(c.isalnum() for c in s)]
        if secnames:
            return max(secnames,key=lambda s: len(s))
        else:
            return str(self) # Use the object's keys as a name

class History(Obj): # Perhaps "Quote" is a better name
    """Information about an Asset that changes over time"""
    stags=['FILE','Asset','DTASOF','UNITPRICE',
        'CURRENCY', # Allows to specificy UNITPRICE in a different CURRENCY than the default
        # defined by the CURRENCY attribute of the Asset class.
    ]
    ktags=[['FILE','Asset','DTASOF']]
    etags=['UNITPRICE','YIELD'] # garnty that there is no conflict with Statement
    dtags=['DTASOF','UNITPRICE','YIELD']

#class Composition(Obj):
    #"""Describe how an asset is made of at a given time, this override
#the gross time-fixed classification done by ASSETCLASS attribute of the
#Asset class. Once an Object is complete, each COMPONENT describe a non
#overlaping part of the asset and what is
#remain is classified as unknown.
#"""
    #stags=['FILE','Asset','DTASOF']
    #mtags=['COMPONENT']
    #ktags=[['FILE','Asset','DTASOF']]
    #etags=['COMPONENT']

#class BondHistory(Obj):
    #"""Information about Assets that change over time."""
    #stags=['FILE','Asset','DTASOF','RATING','YIELD',
        #'DTCOUPON', # Date of next divdend
    #]
    #ktags=[['FILE','Asset','DTASOF',]]
    #etags=['RATING','DTCOUPON']
    
class Org(Obj):
    mtags=['ORG']
    #mtags+=['AccountGroup']
    ktags=[['ORG']]
    
#class AccountGroup(Obj):
    #stags=['ACCTGRP','Org']
    ##mtags=['Account']
    #ktags=[['ACCTGRP']]
    #dtags=['ACCTGRP']
    
class Account(Obj):
    stags=['CURDEF','Org']
    mtags=['ACCTID','ACCTTYPE','ACCTGRP']
    mtags+=['OWNER']  # OFX+
    mtags+=['ACCTMEMO'] # OFX+
    ktags=[['ACCTID'],['ACCTGRP','ACCTTYPE','CURDEF']]
    dtags=['ACCTGRP','CURDEF','ACCTTYPE']
    def iscurrent(self):
        if self.ACCTTYPE:
            return any(accttype.iscurrent() for accttype in self.ACCTTYPE)
        return False

class AccountSource(Obj):
    """ A source that is specialized to give information on a specifice account.
    Override values in SourceClass
    HoldingSource may override DTSTART,DTEND and FULL
    See SourceClass for definition of DTSTART,DTEND,FULL,COMPLETE
    """
    stags=['Account','FILE',
        'DTSTART','DTEND',
        'COMPLETE',
        'FULL']
    ktags=[['Account','FILE']]

class Holding(Obj):
    """Also know as "Position" is a specific Asset held in a specified Account (in a specified bank.)
However, some banks might use several 'Asset's to describe the same thing, while other banks don't
For example current checking account and a deposit program.
As a result writting that the different Assets in one flat line (stating that the two 'Asset's are the same)
should be avoided.
However, by placing each Asset in a seperate line with the same HIDX it is possible                       
at the account (and bank) level to say that they are the same.                       
"""
    stags=['Account']
    mtags=['Asset'] # See comment above on why Asset is mtag
    stags+=['HIDX'] # OFX+ Numerical index to a holding
    mtags+=['POSMEMO'] # OFX+ Need to have a different tag from MEMO which is for transactions
    ktags=[['HIDX'],['Asset','Account']]
    dtags=['Account','Asset']
    def __str__(self):
        if self.HIDX: return str(self.HIDX[0])
        return super(Holding,self).__str__()
    def ischecking(self):
        return any(a.ischecking() for a in self.Asset)
    def isfixedprice(self):
        return any(a.isfixedprice() for a in self.Asset)
    def getsecname(self):
        return max([a.getsecname() for a in self.Asset],key=lambda s: len(s))
    def getassetclass(self):
        assetclass=[]
        for a in self.Asset:
            assetclass.extend(a.ASSETCLASS)
        return list(set(assetclass))
    def getfixedprice(self):
        if self.ischecking(): return 1.
        for a in self.Asset:
            if a.FIXEDPRICE: return a.FIXEDPRICE[0]
    
class HoldingSource(Obj):
    """'DTSTART','DTEND','FULL' (see SourceClass) override the values in
AccountSource which in turn override the value in SourceClass
Note: COMPLETE is meaningless here: The task of COMPLETE
is to say that a holding that does not appear in the source has
zero statement, but, if a holding does not appear there will be
no HoldingSource.
    """
    stags=['FILE','Holding',
        'FULL',
        'DTSTART','DTEND',  # Mandatory if FULL is true
        '_FULL_','_DTSTART_','_DTEND_'
        ]
    ktags=[['FILE','Holding']]
    def full(self): return self._FULL_ and self._FULL_[0]

class Statement(Obj):
    stags=['HoldingSource','DTASOF','UNITS','MKTVAL','POSTYPE']
    stags+=['MKTVALUSD'] # MKTVAL in CURRENCY=USD regardless of what CURRENCY or CURDEF is used
    stags+=['ACCRDINT'] # interest that would be given by the buying side if the asset was to be sold at DTASOF.
    stags+=['ACCRDINTUSD'] # ACCRDINT in USD

    # Adding UNITS or MKTVAL to the key garnty that there is no conflict with History
    ktags=[['HoldingSource','DTASOF']]
    etags=['UNITS','MKTVAL','POSTYPE']
    dtags=['DTASOF','UNITS']

class _OTran_(Obj):   pass
class _MTran_(Obj):   pass

class Transaction(Obj):
    stags=['HoldingSource','DTSETTLE','DTTRADE','DTUSER','TRNTYPE',
        'UNITS', # Chnage in number of units caused by transaction (positive on
        # buy, negative on sell)
        # In checking account this is the change in ORIGCURRENCY
        'UNITPRICE', # The price in which the units where bought/sold (always
        # positive, or missing on splits) 
        # In checking account this is exchange rate from ORIGCURRENCY to CURRENCY
        'COST', # Amount paied for the purchase (or negative number on sell)
        'MARKUP','MARKDOWN','LOAD','COMMISSION','FEES','TAXES','WITHHOLDING',
        'PENALTY','STATEWITHHOLDING',
        'INTEREST', # OFX+ Interest or dividend before fees/commission/tax... this also include ACCRDINT (negative when buy not on issue)
        'TOTAL', # Money moved to checking account (should be negative on buy) (not including ACCRDINT), when moving from one checking account to another this is the money the other checking account will receive.
        # TRNTYPE=BUY/SELL -> TOTAL=-UNITS*(UNITPRICE+MARKUP-MARKDOWN)-(COMMISSION+FEES+LOAD+TAXES+PENALTY+WITHHILDING+STATEWITHHOLDING)
        'ACCRDINT', # If TRNTYPE=BUY/SELL this is real money, paied to/from the the PAYEEID
        'CHECKNUM',
        'INTUNITS', # OFX+ Total number of UNITS for which INTEREST or DIVIDEND was given
        'CURRENCY', # Allows to specificy *all* price values (note, UNITS is not a price value) in the transactions in a different CURRENCY
        # than the currency speicifed by the Asset of the Holding
        'Expense', # Classification of the transaction into an expense category               
        '_MTran_', # The Merged Transaction this transaction is contributing to
        'NLINE',# NLINE distiniguish between different transaction in the same file from the same time.
        # You can have a single transaction spread over multiple lines in the same file if you place the same NLINE in each of these lines
        '_SKIP_', # Transaction that should be ignored
        'XTRNTYPE', 'XACCTID','XSECNAME','XUNITS',
        ]
    mtags=[
        'FITID', # A transaction may have several FITID if they use different domains or sub-domains. Different transactions from the same source may have the same FITID but only if it is for the same Holding, this can be used as a tool to break a single transactions into multiple flat lines.
        'RELFITID','RELSECID', # Reference to other "splits" of the same transaction
        'PAYEEID', # This should give a hint to who TOTAL went or from whom it came 
        '_OTran_', # The Original Transaction(s) from which a merged transaction was composed (see also OLINE)
        'OLINE', # NLINE of Original Transactions (see also _OTran_)
        'OFITID',  # FITID of the original transactions 
        'MEMO','NAME',
        ]
    ktags=[['HoldingSource','DTSETTLE','NLINE'],['HoldingSource','FITID']]
    dtags=['DTSETTLE','DTTRADE','TRNTYPE','UNITS','UNITPRICE','INTEREST','MEMO']
    def dttrade(self):
        return self.DTTRADE[0] if self.DTTRADE else self.DTSETTLE[0]

class OutstandingOrder(Obj):
    """Transaction intiated by the user. Usually this will be a check written down.
    Usually the content of this will be merged into a _MERGED_ regular transaction that matches
    this"""
    stags=['HoldingSource','DTUSER',
        'UNITS', # Chnage in number of units caused by transaction (positive on buy, negative on sell)
        'TOTAL', # Money moved to checking account (should be negative on buy) 
        'CHECKNUM','NLINE',
        'DTSETTLE', # This can appear if an OutstandingOrder appears on the same line as a Transaction
        #'_MTran_', # The Merged Transaction this transaction is contributing to
        ]
    mtags=['MEMO','NAME',]
    ktags=[['HoldingSource','DTUSER','NLINE']]

class Expense(Obj):
    """ Expense category. You should create an expense category by adding a flat line that looks like:
    EXPCAT=<your-name-for-this-category> Transaction=|<transaction-filter>|
    and this will place the Expense object in the _Expense_ attribute of transactions with the me command
    """
    stags=['EXPCAT']
    mtags=['TransactionFILTER']
    ktags=[['EXPCAT']]
    
#class Report(Obj):
    #"""Sum (TOTAL,CURRENCY) some aspect (RTYPE) of an Account for a specified time interval (DTSTART,DTEND) """
    #stags=['Account','DTSTART','DTEND',
        #'RTYPE'] # Uniquely identify what is being reported. Can be any string but try to use the same thing for the same report. Keep all other text in MEMO
    #stags+=['TOTAL','CURRENCY'] # By default the account's CURDEF is used
    #mtags=['MEMO']
    #ktags=[['Account','DTSTART','DTEND','RTYPE']]
